import datetime
import json
import time
from collections import defaultdict
from urllib import parse

import arrow
from flask import current_app as app

from app.config.settings import SCANVHOST
from app.db import redis
from app.db.models.alert_groups import AlertGroup
from app.db.models.alertreceivers import AlertReceivers
from app.db.models.assets import Asset
from app.db.models.jobs import Job
from app.db.models.notifications import Notifications
from app.db.models.notificationsendlogs import NotificationSendLog
from app.db.models.users import User
from app.db.models.alerts import Alert
from app.db.models.asset_tasks import AssetTasks
from app.libs.sendcloud import SendCloud
from app.libs.wechat import WeChat
from app.libs.yunpian import YunPian
from app.libs.score import Score
from app.libs.enums import (
    WS_TASK_GROUP_TUPLE,
    AVAILABLE_TASK_GROUP_TUPLE,
    TaskType,
    WEAKNESS_TASK_GROUP_TUPLE
)


CATEGORY_ALERT_TASK_TYPE = (
    TaskType.security_event.value,
    TaskType.asset.value,
    TaskType.content.value,
    TaskType.ssl.value,
)

LEVEL_ALERT_TASK_TYPE = (
    TaskType.change_check.value,
    TaskType.vul.value,
    TaskType.hostVul.value,
)

SEC_CATEGORY_NAME_MAP = {
    "black_links": '暗链',
    "broken_links": '坏链',
    "cryjack": '挖矿',
    "malscan": '挂马',
    "foreign_links": '风险外链',
}
SSL_CATEGORY_NAME_MAP = {
    "certificate": "配置异常",
    "protocol": "协议漏洞"
}
CONTENT_CATEGORY_NAME_MAP = {
    "keyword": "敏感词",
    "privacy_disclosure": "隐私信息",
}
CHANGE_CHECK_CATEGORY_NAME_MAP = {
    "text": "内容",
    "resource": "资源",
    "structure": "结构",
}
LEVEL_NAME_MAP = {
    5: "严重",
    4: "高危",
    3: "中危",
    2: "低危",
    1: "低危",
    0: "低危",
}


class AlertSender:
    send_cloud = SendCloud()
    yunpian = YunPian()
    wechat = WeChat()
    task_type = TaskType.content.value
    task_category = "content"
    task_name = '内容违规监测'
    warning_name = '内容威胁'
    email_template = 'scanvmax_content_notification_v2'
    sms_template = '5712712'
    wechat_template = 'Iqwp3-LMT9Eq2GjOb8bD3v68hsFDvgGaawp1mQ05Ny4'

    def __init__(self, task, warnings=None):
        self.task = task
        self.warnings = self.task.result.warnings if not warnings else warnings
        self.asset_task = AssetTasks.objects.find_one({f"{task.taskType}Settings.taskId": task.taskId})
        self.security_status = self.task.securityStatus
        self.is_schedule = False if self.task.triggerType == 'manual' else True
        self.user = User.objects.filter(pk=task.uid).only('username', 'wechat').first()
        self.job = Job.objects.filter(pk=task.jobId).only(
            'note', 'targetUrl', 'enableAlert', 'alertSettings', 'securityStatus',
            *(f"{t}AlertState" for t in WS_TASK_GROUP_TUPLE + AVAILABLE_TASK_GROUP_TUPLE)
        ).first()
        self.email_param = {}
        self.sms_param = ""
        self.wechat_param = {}
        self.inbox_param = {}
        self.inbox_sup_param = {}
        self.end_time = self.get_end_time()
        self.warning_count = self.task.result.warningCount
        # 漏洞等级或者漏洞类型数量
        self.warning_type_map = self.parse_task_warnings()
        self.name = self.job.note
        self.url = self.job.targetUrl
        self.alert_groups = self.get_alert_groups()
        self.alert_receiver_ids = self.alert_groups.distinct('receiverIds')
        self.alert_group = self.alert_groups.first()
        self.alert_receivers = self.get_alert_receivers()
        self.sms_receivers = self.get_sms_receivers()
        self.email_receivers = self.get_email_receivers()
        self.wechat_receivers = self.get_wechat_receivers()
        # self.inbox = self.get_inbox()
        self.title = f'发现业务系统存在{self.warning_name}'
        if self.task_category == "aviliable":
            self.summary = f"{self.name}（{self.url}）监测的{self.task_name}任务：{self.task.name}（{self.task.target}），于{self.end_time}发现{self.task.result.addition.get('nodeAbnormalCount', 0)}个监测点异常，请及时修复。"
        else:
            self.summary = f"{self.name}（{self.url}）于{self.end_time}{self.task_name}发现{self.warning_count}个{self.warning_name}，请及时修复。"
        app.logger.info(f'####处理task告警, task_id: {self.task.pk}')

    def notification_send_log(self, content):
        notificationsendlog = NotificationSendLog(
            status='pending',
            title=self.title,
            summary=self.summary,
            receivers=[],
            content=content,
            refId=self.task.refId,
            domain=self.task.domain,
            type='monitorException',
            taskCategory=self.task_category,
            taskType=self.task_type,
            target=self.task.target,
            uid=self.user.pk
        )
        notificationsendlog.save()
        return notificationsendlog

    def get_end_time(self):
        return arrow.Arrow.fromdatetime(self.task.endTime).shift(hours=8).format('YYYY-MM-DD HH:mm:ss')

    def get_alert_groups(self):
        return AlertGroup.objects.filter(pk__in=self.task.alertSettings.alertGroupIds, enable=True)

    def get_alert_receivers(self):
        return AlertReceivers.objects.filter(pk__in=self.alert_receiver_ids, enable=True)

    def check_email_allowed_time(self, emails):
        """
        周期任务 判断邮件是否在允许的时间内
        :return:
        """
        allow_time = self.alert_group.alertSettings.emailAllowedTime
        return emails if allow_time.get('start', '00:00:00') <= arrow.utcnow().shift(hours=8).format(
            'HH:mm:ss') <= allow_time.get('end', '23:59:59') else []

    def check_email_daily_limit(self, emails):
        """
        周期任务 判断邮件是否超过上限 task.alertSettings.emailDailyLimit
        :param emails:
        :return:
        """
        if self.job_today_alert.get("emailCount") >= self.alert_group.alertSettings.emailDailyLimit:
            app.logger.warning(f"超过每日邮件告警数量限制： jobId:{self.job.id}")
            return []
        return emails

    def check_email_receivers(self, emails):
        emails = self.check_email_allowed_time(emails)
        emails = self.check_email_daily_limit(emails)
        return emails

    def check_sms_allowed_time(self, smss):
        """
        周期任务 判断短信是否在允许的时间内
        param smss:
        :return:
        """
        allow_time = self.alert_group.alertSettings.smsAllowedTime
        return smss if allow_time.get('start', '00:00:00') <= arrow.utcnow().shift(hours=8).format(
            'HH:mm:ss') <= allow_time.get('end', '23:59:59') else []

    def check_sms_daily_limit(self, smss):
        """
        周期任务 判断邮件是否超过上限 task.alertSettings.smsDailyLimit todo
        :param smss:
        :return:
        """
        if self.job_today_alert.get("smsCount", 0) >= self.alert_group.alertSettings.smsDailyLimit:
            app.logger.warning(f"超过每日短信告警数量限制：jobId:{self.job.id}, limit: {self.alert_group.alertSettings.smsDailyLimit}")
            return []
        return smss

    def check_sms_receivers(self, smss):
        smss = self.check_sms_allowed_time(smss)
        smss = self.check_sms_daily_limit(smss)
        return smss

    def get_email_receivers(self):
        """
        可以发送邮件的对象
        :return:
        """
        if not bool(self.alert_group.alertSettings.notification.get('email')):
            return []
        email_receivers = self.alert_receivers.filter(emailVerified=True).distinct('email')
        email_receivers = self.check_email_receivers(email_receivers)
        if email_receivers:
            self._get_email_param()
        return email_receivers

    def get_sms_receivers(self):
        """
        可以发送短信的对象
        :return:
        """
        if not bool(self.alert_group.alertSettings.notification.get('sms')):
            return []
        sms_receivers = self.alert_receivers.filter(phoneVerified=True).distinct('phone')
        sms_receivers = self.check_sms_receivers(sms_receivers)
        if sms_receivers:
            self._get_sms_param()
        return sms_receivers

    def get_wechat_receivers(self):
        """
        可以发送微信的对象
        :return:
        """
        if not bool(self.alert_group.alertSettings.notification.get('wechat')):
            return []
        if self.is_schedule and not self.alert_group.enable:
            return []
        if not (wx_openid := self.user.wechat.get('wx_openid', '')):
            return []
        self._get_wechat_param()
        return [wx_openid]

    def get_inbox(self):
        in_box = self.alert_group.alertSettings.notification.get('inBox')
        if in_box is False:
            return False
        else:
            self._get_inbox_param()
            return True

    @property
    def detail_link(self):
        return f"{SCANVHOST}/"

    def get_collect_type(self, trigger_type, collect):
        if trigger_type == 'schedule':
            return "周期性全量监测" if collect.get("type") == "full" else "周期性监测"
        elif trigger_type == 'scheduleIncrement':
            return '周期性增量监测'

        elif trigger_type == 'manual':
            return '单次监测'

        elif trigger_type == 'special':
            return '专项监测'

    def _get_email_param(self):
        self.email_param = {
            '%user%': [self.user.username],
            '%name%': [self.name],
            '%time%': [self.end_time],
            '%dz%': [self.url],
            '%type%': [self.get_collect_type(self.task.triggerType, self.task.taskSettings.collect)],
            '%num%': [str(self.warning_count)],
            '%detail%': [self.detail_link],
            '%content%': [self._get_wechat_keyword3()],
        }

    def _get_sms_param(self):
        self.sms_param = f"#domain#={self.name}（{self.url}）&#time#={self.end_time}&#way#={self.get_collect_type(self.task.triggerType, self.task.taskSettings.collect)}&#count#={str(self.warning_count)}"

    def _get_wechat_keyword3(self):
        keyword3 = []
        detail_map = {
            TaskType.vul.value: LEVEL_NAME_MAP,
            TaskType.hostVul.value: LEVEL_NAME_MAP,
            TaskType.change_check.value: LEVEL_NAME_MAP,
            TaskType.content.value: CONTENT_CATEGORY_NAME_MAP,
            TaskType.ssl.value: SSL_CATEGORY_NAME_MAP,
            TaskType.security_event.value: SEC_CATEGORY_NAME_MAP
        }
        for k, v in detail_map.get(self.task.taskType, {}).items():
            if count := self.warning_type_map.get(k, 0):
                keyword3.append(f"{v}{count}个")
        return "，".join(keyword3)

    def _get_wechat_content(self):
        kw1 = "内容违规告警"
        kw3 = f"发现违规事件{self.warning_count}个：{self._get_wechat_keyword3()}。"
        kw4 = f"存在{self.warning_count}个内容违规事件，请及时处理。"
        return kw1, kw3, kw4

    def _get_wechat_param(self):
        kw1, kw3, kw4 = self._get_wechat_content()
        if self.task.taskType not in AVAILABLE_TASK_GROUP_TUPLE:
            kw4 = f"{self.name}（{self.url}）{kw4}"
        self.wechat_param = {
            # 系统名称
            "keyword1": {
                "value": f"ScanV（云监测）{kw1}"
            },
            # 日期
            "keyword2": {
                "value": self.end_time,
            },
            # 类型
            "keyword3": {
                "value": kw3
            },
            # 监测结果
            "keyword4": {
                "value": kw4,
            }
        }

    def get_inbox_sup_param(self):
        data = {}
        if asset_task := self.asset_task:
            data.update({'asset_task_id': asset_task.id})
            if asset_task.taskGroupType == "web_security":
                data.update({'is_result': True if asset_task.lastResult else False})
            else:
                data.update({'is_result': True})
            security_status = getattr(self.job, "securityStatus", {})
            data.update({"is_checked": True if security_status.get("update_time", "") else False})
        self.inbox_sup_param = data
        return data

    def _get_inbox_param(self):
        self.inbox_param = {
            'to': [self.user.pk],
            'title': self.title,
            'message': self.summary,
            'data': {
                'securityStatus': self.security_status,
                'taskCategory': self.task_category,
                'warningCount': self.warning_count,
                'taskType': self.task_type,
                'taskId': self.task.pk,
                'refId': self.task.refId,
                'jobId': self.task.jobId,
                'target': self.task.target or self.job.targetUrl,
                'triggerType': self.task.triggerType,
            },
            'choosedActionMap': {
                str(self.user.pk): False
            },
            'isMarkedMap': {
                str(self.user.pk): False
            },
            'datetime': arrow.utcnow().datetime
        }
        if self.asset_task:
            self.inbox_param["data"].update(self.inbox_sup_param)

    @property
    def job_today_alert(self):
        """
        如果redis存在当天的告警数量记录，则直接返回；
        如果存在记录但日期非当天日期，则重置；
        若不存在记录，则重置"""
        # 增加读锁，同一个job同一时刻只能一个线程在读
        lock_key = f"lock:dailyAlert:{str(self.job.id)}"
        while True:
            # 获取读锁，2秒有效期
            if redis.set(lock_key, 1, ex=2, nx=1):

                job_key = f"dailyAlert:{str(self.job.id)}"
                base_today_alert = {
                        "timestamp": self.today_start_timestamp(),
                        "smsCount": 0,
                        "emailCount": 0
                }
                # 查询该job在redis中的今日告警记录
                job_today_alert = redis.get(job_key)
                if job_today_alert:
                    job_today_alert = json.loads(job_today_alert)
                    # 若告警记录存在但是非当天日期，则重置返回
                    if job_today_alert.get("timestamp") != self.today_start_timestamp():
                        redis.set(job_key, json.dumps(base_today_alert), ex=60 * 60 * 24)
                        result = base_today_alert
                    else:
                        # 存在今日告警记录直接返回
                        result = job_today_alert
                # 未查询到今日告警记录，重置返回
                else:
                    redis.set(job_key, json.dumps(base_today_alert), ex=60 * 60 * 24)
                    result = base_today_alert
                redis.delete(lock_key)
                return result
            else:
                app.logger.warning(f"等待读锁{lock_key}释放")
                time.sleep(1)

    def _inc_job_today_alert(self, count_key):
        job_key = f"dailyAlert:{str(self.job.id)}"
        job_today_alert = self.job_today_alert
        job_today_alert[count_key] += 1
        redis.set(job_key, json.dumps(job_today_alert), ex=60 * 60 * 24)

    def inc_email_today_alert(self):
        self._inc_job_today_alert(count_key="emailCount")

    def inc_sms_today_alert(self):
        self._inc_job_today_alert(count_key="smsCount")

    @staticmethod
    def today_start_timestamp():
        """当天零点时间戳"""
        return int(time.mktime(datetime.date.today().timetuple()))

    def send_email(self):
        # notification_send_log =
        is_send = False
        for receiver in self.email_receivers:
            args = {
                'to': [receiver],
                'sub': self.email_param
            }
            try:
                status, resp_text = self.send_cloud.sendmail(self.email_template, args)
                app.logger.info(f'邮件发送结果：status: {status}, to receiver: {receiver}, template: {self.email_template}，resp: {resp_text}')
                if status == "success":
                    is_send = True
            except Exception as e:
                app.logger.info("邮件发送失败：", e, self.email_template, args)
        # 增加今日已告警次数
        if is_send is True:
            self.inc_email_today_alert()

    def send_sms(self):
        is_send = False
        for receiver in self.sms_receivers:
            try:
                status, res_msg = self.yunpian.tpl_send(receiver, self.sms_template, self.sms_param)
                app.logger.info(f"短信发送结果：status: {status}, to receiver: {receiver}, template: {self.sms_template}, resp: {res_msg}")
                if status == "success":
                    is_send = True
            except Exception as e:
                app.logger.info("短信发送失败：", e, self.sms_template, self.sms_param)
        # 增加今日已告警次数
        if is_send is True:
            self.inc_sms_today_alert()

    def send_wechat(self):
        wx_url = self.detail_link
        for receiver in self.wechat_receivers:
            try:
                resp = self.wechat.send_tpl_msg(self.wechat_template, receiver, wx_url, self.wechat_param)
                app.logger.info(f'微信发送结果, to receiver: {receiver}, template: {self.wechat_template}, resp: {resp}')
            except Exception as e:
                app.logger.info("微信发送失败:", e, self.wechat_template, receiver, wx_url, self.wechat_param)
        return

    def send_inbox(self):
        notifications = Notifications(**self.inbox_param)
        notifications.save()
        app.logger.info(f'站内信发送成功, to inbox success')

    def save_alert(self, **kwargs):
        notification = self.alert_group.alertSettings.notification
        if notification.get('email') or notification.get('sms') or notification.get('wechat') or (
                notification.get('inBox') is not False):
            task_category = self.task_category
            if self.task_type in WEAKNESS_TASK_GROUP_TUPLE:
                task_category = "weakness"
            asset_task_id = ''
            if self.asset_task:
                asset_task_id = self.asset_task.pk
            data = {
                'securityStatus': self.security_status,
                'taskCategory': task_category,
                'warningCount': self.warning_count,
                'taskType': self.task_type,
                'taskId': self.task.pk,
                'refId': self.task.refId,
                'jobId': self.task.jobId,
                'target': self.task.target or self.job.targetUrl,
                'triggerType': self.task.triggerType,
                'asset_task_id': asset_task_id,
            }
            if asset_task := self.asset_task:
                data.update({'asset_task_id': asset_task.id})
                if asset_task.taskGroupType == "web_security":
                    data.update({'is_result': True if asset_task.lastResult else False})
                else:
                    data.update({'is_result': True})
                security_status = getattr(self.job, "securityStatus", {})
                data.update({"is_checked": True if security_status.get("update_time", "") else False})

            alert = Alert(
                securityStatus=self.security_status,
                taskTriggerType=self.task.triggerType,
                title=self.title,
                summary=self.summary,
                content=self.summary,
                detail={
                    "securityStatus": self.security_status,
                    "taskCategory": task_category,
                    "warningCount": self.warning_count,
                    "taskType": self.task_type,
                    "taskId": self.task.pk,
                    "refId": self.task.refId,
                    'data': data,
                },
                taskType=self.task_type,
                domain=self.task.domain,
                target=self.job.targetUrl,
                jobId=self.job.pk,
                refId=self.job.pk,
                taskId=self.task.pk,
                taskCategory=task_category,
                uid=self.task.uid,
                taskName=self.task.name,
                alertType='abnormal' if self.task.securityStatus == 'warning' else 'normal',
                duration=kwargs.get("duration", 0)
            )
            alert.save()

    def alert_state_and_status(self):
        alert_state = {"sendCount": 0}
        if self.task_type == "asset":
            asset = Asset.objects.with_id(self.task.refId)
            alert_state = asset.alertState
        elif self.task_type in AVAILABLE_TASK_GROUP_TUPLE:
            task_idx = self.task.taskIdx or 0
            alert_state_key = f"{self.task_type}AlertState"
            try:
                alert_state = getattr(self.job, alert_state_key)[task_idx].to_dict()
            except IndexError:
                alert_state = {}
        elif self.task_type in (TaskType.hostVul.value, ):
            alert_state_key = f"{self.task_type}AlertState"
            if hasattr(self.asset_task, alert_state_key):
                alert_state = getattr(self.asset_task, alert_state_key)
        else:
            alert_state_key = f"{self.task_type}AlertState"
            if hasattr(self.job, alert_state_key):
                alert_state = getattr(self.job, alert_state_key)

        if alert_state.get("securityStatus") == 'warning' and self.security_status == 'safe':
            # 异常恢复
            self.security_status = 'abnormalResolved'

        if not alert_state.get("firstSendTime"):
            alert_state["firstSendTime"] = self.task.endTime

        alert_state["lastSendTime"] = self.task.endTime
        alert_state["sendCount"] = alert_state.get("sendCount", 0) + 1
        alert_state["securityStatus"] = self.security_status

        return alert_state, self.security_status

    def update_asset_task_alert_state(self, security_status, alert_state):
        """主机监测任务告警相关内容更新在asset_task表"""
        alert_state_update_key = f"{self.task_type}AlertState"
        if security_status == 'abnormalResolved':
            AssetTasks._get_collection().update_one({
                "_id": self.asset_task.id,
            }, {
                "$set": {alert_state_update_key: {
                    "firstSendTime": None,
                    "endSendTime": None,
                    "sendCount": 0,
                    "securityStatus": 'safe',
                }}
            })
        else:
            AssetTasks._get_collection().update_one({
                "_id": self.job.id,
            }, {
                "$set": {alert_state_update_key: alert_state}
            })

    def update_job_alert_state(self, security_status, alert_state):
        alert_state_update_key = f"{self.task_type}AlertState.{self.task.taskIdx}" if self.task_category == "aviliable" else f"{self.task_type}AlertState"
        if self.task_type == TaskType.http.value:
            # http任务强制重写httpAlertState，保持与httpSettings长度一致
            job = Job._get_collection().find_one({"_id": self.job.id}, {"httpAlertState": 1, "httpSettings": 1})
            settings_length = len(job.get("httpSettings", []))
            state = job.get("httpAlertState", [])
            state_length = len(state)
            for index, value in enumerate(state):
                if not value:
                    state[index] = {
                        "firstSendTime": None,
                        "endSendTime": None,
                        "sendCount": 0,
                        "securityStatus": 'safe',
                    }
            if settings_length > state_length:
                for _ in range(settings_length - state_length):
                    state.append({
                        "firstSendTime": None,
                        "endSendTime": None,
                        "sendCount": 0,
                        "securityStatus": 'safe',
                    })
            Job._get_collection().update_one({
                "_id": self.job.id,
            }, {
                "$set": {"httpAlertState": state}
            })
        if security_status == 'abnormalResolved':
            Job._get_collection().update_one({
                "_id": self.job.id,
            }, {
                "$set": {alert_state_update_key: {
                    "firstSendTime": None,
                    "endSendTime": None,
                    "sendCount": 0,
                    "securityStatus": 'safe',
                }}
            })
        else:
            Job._get_collection().update_one({
                "_id": self.job.id,
            }, {
                "$set": {alert_state_update_key: alert_state}
            })

    def get_duration(self, security_status, alert_state):
        duration = 0
        if security_status == 'abnormalResolved':
            first = alert_state.get("firstSendTime")
            last = alert_state.get("lastSendTime")
            if all([first, last]):
                duration = int((last.timestamp() - first.timestamp()) * 1000)
        return duration

    def check_task_alert_limit(self, alert_state):
        """连续的可用性监测告警数量不超过限制，可用性恢复告警不在限制内"""
        alert_count = self.task.taskSettings.alert.get("alertCount")
        if self.security_status != 'abnormalResolved' \
                and (self.task.triggerType == 'schedule' or self.task.triggerType == 'scheduleIncrement') \
                and alert_count:
            # 异常告警次数限制
            if alert_state.get("sendCount", 0) > alert_count:
                app.logger.warning(f"task:{self.task.id}:超过任务异常告警次数限制，不发送告警通知")
                return False
        return True

    def check_node_alert_limit(self, security_status):
        """ 可用性任务小于设置的告警节点数不处理"""
        if self.task_category == "aviliable" and \
                security_status == 'warning' and \
                self.task.result.addition.get("nodeAbnormalCount", 0) < self.task.taskSettings.alert.get("nodeCount", 0):
            # 小于设置的最小告警节点数
            app.logger.info(f"task:{self.task.id}, 异常节点小于最小数量跳过告警")
            return False
        return True

    def update_notifications(self):
        sup_param = self.get_inbox_sup_param()
        if is_result := sup_param.get("is_result") and self.asset_task.taskGroupType == "web_security":
            asset_task_id = self.asset_task.id
            Notifications.objects.filter(data__asset_task_id=asset_task_id).update(data__is_result=is_result)
        if is_checked := sup_param.get("is_checked"):
            Notifications.objects.filter(data__jobId=self.task.jobId).update(data__is_checked=is_checked)

    def parse_task_warnings(self):
        warning_type_map = {}
        if self.task.taskType in CATEGORY_ALERT_TASK_TYPE:
            for warn in self.warnings:
                if not warning_type_map.get(warn.category):
                    warning_type_map[warn.category] = 1
                else:
                    warning_type_map[warn.category] += 1
        elif self.task.taskType in LEVEL_ALERT_TASK_TYPE:
            for warn in self.warnings:
                level = int(warn.level)
                if not warning_type_map.get(level):
                    warning_type_map[level] = 1
                else:
                    warning_type_map[level] += 1
        return warning_type_map

    def check_alert_detail(self):
        if self.task.taskType not in WS_TASK_GROUP_TUPLE + (TaskType.hostVul.value, ):
            return True, {}, {}
        alert_detail = set(getattr(self.alert_group.alertEnableDetail, self.task.taskType, []))
        if not alert_detail:
            return False, alert_detail, {}
        task_detail = set()
        if self.task.taskType in CATEGORY_ALERT_TASK_TYPE:
            task_detail = set(self.warning_type_map.keys())
        elif self.task.taskType in LEVEL_ALERT_TASK_TYPE:
            task_detail = {Score.severityMap.get(int(i), {}).get("key", "").lower() for i in set(self.warning_type_map.keys())}
        if alert_detail & task_detail:
            return True, alert_detail, task_detail
        return False, alert_detail, task_detail

    def send_alerts(self):
        self.update_notifications()
        # 如果任务没有开告警, 则直接返回
        if not self.task.alertSettings.alertGroupIds or not self.alert_group.enable:
            app.logger.warning(f"job:{self.job.id}:任务资产未开启告警，不发送告警通知")
            return

        # 任务类型告警开关未打开，则不告警
        if not self.alert_group.alertSettings.enable.get(self.task_type):
            app.logger.warning(f"task:{self.task.id}:任务类型未开启{self.task_type}告警，不发送告警通知")
            return
        alert_detail_flag, alert_detail, task_detail = self.check_alert_detail()
        if not alert_detail_flag:
            app.logger.warning(f"task:{self.task.id}:任务未达到告警条件，告警条件{alert_detail}, 任务结果{task_detail}, 不发送告警通知")
            return

        alert_state, self.security_status = self.alert_state_and_status()
        if not alert_state:
            app.logger.error(f"task:{self.task.id}:任务结果告警状态异常：告警状态：{self.security_status}，任务类型：{self.task_category}，不发送告警通知")
            return
        # 异常恢复仅处理可用性任务，其他类型不处理
        if self.security_status == 'safe' or (self.security_status == 'abnormalResolved' and self.task_category != "aviliable"):
            app.logger.warning(f"task:{self.task.id}:任务结果告警状态不需要告警：告警状态：{self.security_status}，任务类型：{self.task_category}，不发送告警通知")
            return

        # 任务异常告警次数限制(异常恢复告警时不限制)
        if not self.check_task_alert_limit(alert_state):
            return

        duration = self.get_duration(self.security_status, alert_state)
        # 可用性任务异常恢复特殊处理通知文本
        if self.security_status == 'abnormalResolved':
            self.title = f"发现业务系统{self.resolved_name}"
            self.summary = f"{self.name}（{self.url}）监测的{self.task_name}任务：{self.task.name}（{self.task.target}），于{self.end_time}恢复正常。"
        else:      # 如果不是异常恢复且没有告警数量, 则直接返回
            if self.warning_count <= 0:
                app.logger.warning(f"task:{self.task.id}:任务warningCount等于0，不发送告警通知")
                return

        # 可用性任务小于设置的告警节点数不处理
        if not self.check_node_alert_limit(self.security_status):
            return

        self.send_email()
        self.send_sms()
        self.send_wechat()
        if self.get_inbox():
            self.send_inbox()
        self.save_alert(duration=duration)
        self.task.update(isSendAlert=True)
        # 可用性任务异常恢复告警（特殊处理）
        if self.task_type in ["hostVul", ]:
            self.update_asset_task_alert_state(self.security_status, alert_state)
        else:
            self.update_job_alert_state(self.security_status, alert_state)


class ContentAlertSender(AlertSender):

    @property
    def detail_link(self):
        if self.asset_task:
            if self.is_schedule:
                link = f'{SCANVHOST}/#/job-manage/web-task/history/?asset_task_id={str(self.asset_task.id)}&task_id={str(self.task.id)}&task_types=content'
            else:
                link = f'{SCANVHOST}/#/job-manage/web-task/detail?jid={str(self.task.jobId)}&id={str(self.asset_task.id)}&type={self.task.triggerType}&tag=content'
        else:
            link = f'{SCANVHOST}/#/job-manage/web-task?target_url={self.job.targetUrl}'
        return link


class VulAlertSender(AlertSender):
    task_type = 'vul'
    task_category = 'vul'
    task_name = 'Web漏洞监测'
    warning_name = 'Web漏洞'
    email_template = 'scanvmax_vul_notification_v2'
    sms_template = '5712710'

    def _get_wechat_content(self):
        kw1 = "Web漏洞告警"
        kw3 = f"发现漏洞{self.warning_count}个：{self._get_wechat_keyword3()}。"
        kw4 = f"存在{self.warning_count}个Web漏洞威胁，请及时修复。"
        return kw1, kw3, kw4

    @property
    def detail_link(self):
        if self.asset_task:
            if self.is_schedule:
                link = f'{SCANVHOST}/#/job-manage/web-task/history/?asset_task_id={str(self.asset_task.id)}&task_id={str(self.task.id)}&task_types=vul'
            else:
                link = f'{SCANVHOST}/#/job-manage/web-task/detail?jid={str(self.task.jobId)}&id={str(self.asset_task.id)}&type={self.task.triggerType}&tag=vul'
        else:
            link = f'{SCANVHOST}/#/job-manage/web-task?target_url={self.job.targetUrl}'
        return link

    def _get_email_param(self):
        self.email_param = {
            '%user%': [self.user.username],
            '%name%': [self.name],
            '%time%': [self.end_time],
            '%dz%': [self.url],
            '%type%': [self.get_collect_type(self.task.triggerType, self.task.taskSettings.collect)],
            '%count%': [str(len(self.task.result.warnings))],
            '%content%': [self._get_wechat_keyword3()],
            '%detail%': [self.detail_link]
        }


class SslAlertSender(VulAlertSender):
    task_type = 'ssl'
    task_category = 'ssl'
    task_name = 'SSL安全监测'
    warning_name = 'SSL安全漏洞'
    email_template = 'scanvmax_ssl_notification_v2'
    sms_template = '5712708'

    @property
    def detail_link(self):
        if self.asset_task:
            if self.is_schedule:
                link = f'{SCANVHOST}/#/job-manage/web-task/history/?asset_task_id={str(self.asset_task.id)}&task_id={str(self.task.id)}&task_types=ssl'
            else:
                link = f'{SCANVHOST}/#/job-manage/web-task/detail?jid={str(self.task.jobId)}&id={str(self.asset_task.id)}&type={self.task.triggerType}&tag=ssl'
        else:
            link = f'{SCANVHOST}/#/job-manage/web-task?target_url={self.job.targetUrl}'
        return link

    def _get_wechat_content(self):
        kw1 = "SSL安全威胁告警"
        kw3 = f"发现威胁{self.warning_count}个：{self._get_wechat_keyword3()}。"
        kw4 = f"存在{self.warning_count}个SSL安全威胁，请及时修复。"
        return kw1, kw3, kw4

    def _get_email_param(self):
        self.email_param = {
            '%user%': [self.user.username],
            '%name%': [self.name],
            '%time%': [self.end_time],
            '%dz%': [self.url],
            '%type%': [self.get_collect_type(self.task.triggerType, self.task.taskSettings.collect)],
            '%content%': [self._get_wechat_keyword3()],
            '%detail%': [self.detail_link]
        }


class SecurityEventAlertSender(VulAlertSender):
    task_type = 'securityEvent'
    task_category = 'securityEvent'
    task_name = '安全事件监测'
    warning_name = '安全事件'
    email_template = 'scanvmax_securityEvent_notification_v2'
    sms_template = '5712714'

    @property
    def detail_link(self):
        if self.asset_task:
            if self.is_schedule:
                link = f'{SCANVHOST}/#/job-manage/web-task/history/?asset_task_id={str(self.asset_task.id)}&task_id={str(self.task.id)}&task_types=security_event'
            else:
                link = f'{SCANVHOST}/#/job-manage/web-task/detail?jid={str(self.task.jobId)}&id={str(self.asset_task.id)}&type={self.task.triggerType}&tag=security_event'
        else:
            link = f'{SCANVHOST}/#/job-manage/web-task?target_url={self.job.targetUrl}'
        return link

    def _get_wechat_content(self):
        kw1 = "安全事件告警"
        kw3 = f"发现安全事件{self.warning_count}个：{self._get_wechat_keyword3()}。"
        kw4 = f"存在{self.warning_count}个安全事件威胁，请及时处理。"
        return kw1, kw3, kw4


class AssetAlertSender(VulAlertSender):
    task_type = 'asset'
    task_category = 'asset'
    task_name = '资产变动监测'
    warning_name = '资产变动'
    email_template = 'scanvmax_asset_notification_v2'
    sms_template = '5712590'

    @property
    def detail_link(self):
        return f'{SCANVHOST}/#/asset-manage/manage/web'

    def _get_sms_param(self):
        self.sms_param = f"#domain#={self.name}（{self.url}）&#time#={self.end_time}"

    def _get_wechat_keyword3(self):
        keyword3 = []
        if self.warning_type_map.get("port"):
            keyword3.append("端口")
        if self.warning_type_map.get("subdomain"):
            keyword3.append("子域名")
        return "，".join(keyword3)

    def _get_wechat_content(self):
        kw1 = "资产变动告警"
        kw3 = f"发现资产{self._get_wechat_keyword3()}有变动。"
        kw4 = f"有更新，请及时确认。"
        return kw1, kw3, kw4

    def _get_email_param(self):
        self.email_param = {
            '%user%': [self.user.username],
            '%name%': [self.name],
            '%time%': [self.end_time],
            '%dz%': [self.url],
            '%type%': [self.get_collect_type(self.task.triggerType, self.task.taskSettings.collect)],
            '%content%': [self._get_wechat_keyword3()],
            '%detail%': [self.detail_link]
        }


class HttpAlertSender(VulAlertSender):
    """自定义html模板发送"""
    task_type = 'http'
    task_category = 'aviliable'
    task_name = 'HTTP可用性监测'
    resolved_name = 'HTTP可用性恢复'
    warning_name = 'HTTP可用性异常'
    email_template = ''

    @property
    def detail_link(self):
        return f'{SCANVHOST}/#/job-manage/aviliable-task/detail?id={str(self.asset_task.id)}&type={self.task.triggerType}&jid={str(self.task.jobId)}'

    def _get_sms_param(self):
        num = self.task.result.addition.get("nodeAbnormalCount", 0)
        self.sms_template = '5712592'
        self.sms_param = f"#domain#={self.name}（{self.url}）&#url#={self.task.name}（{parse.quote(self.task.target)}）&#time#={self.end_time}&#type#={self.task.taskType}&#count#={num}"
        if self.task.securityStatus != "warning":
            # 异常恢复
            self.sms_template = '5712594'
            self.sms_param = f"#domain#={self.name}（{self.url}）&#url#={self.task.name}（{parse.quote(self.task.target)}）&#time#={self.end_time}&#type#={self.task.taskType}"

    def _get_wechat_content(self):
        kw3 = f"{self.job.note}（{self.job.targetUrl}）HTTP任务" if self.job.note else f"{self.job.targetUrl} HTTP任务"
        task_info = f"{self.task.name}（{self.task.target}）" if self.task.name else self.task.target
        if self.task.securityStatus == "warning":
            kw1 = "HTTP任务异常告警"
            kw4 = f"{task_info}可用性异常，请及时确认。"
        else:
            kw1 = "HTTP任务异常恢复告警"
            kw4 = f"{task_info}可用性恢复正常，请及时确认。"
        return kw1, kw3, kw4

    def send_email(self):
        alert_state, security_status = self.alert_state_and_status()
        if not self.email_receivers:
            return

        html = self._get_html(alert_state, security_status, **self.email_param)
        is_send = False
        subject = f"【ScanV】发现业务系统存在{self.warning_name}"
        if security_status == "abnormalResolved":
            subject = f"【ScanV】发现业务系统{self.resolved_name}"
        for receiver in self.email_receivers:
            try:
                status, resp_text = self.send_cloud.simple_sendmail(to_user=[receiver],
                                                                    subject=subject,
                                                                    html=html)
                app.logger.info(
                    f'邮件发送结果：status: {status}, to receiver: {receiver}, template: {self.email_template}, param: {self.email_param}，resp: {resp_text}')
                if status == "success":
                    is_send = True
            except Exception as e:
                app.logger.info("邮件发送失败：", e, self.email_template, self.email_param)

        # 增加今日已告警次数
        if is_send is True:
            self.inc_email_today_alert()

    @staticmethod
    def _get_format_duration(sec):
        detail = datetime.timedelta(seconds=sec)
        if detail.days == 0:
            return "".join(["0天", str(detail).replace(":", "小时", 1).replace(":", "分钟", 1), "秒"])
        elif detail.days == 1:
            return "".join([str(detail).replace(" day, ", "天", 1).replace(":", "小时", 1).replace(":", "分钟", 1), "秒"])
        else:
            return "".join([str(detail).replace(" days, ", "天", 1).replace(":", "小时", 1).replace(":", "分钟", 1), "秒"])

    def _get_html(self, alert_state, security_status, **kwargs):
        duration = self.get_duration(security_status, alert_state)
        duration = self._get_format_duration(sec=round(duration/1000))
        if security_status == "abnormalResolved":
            html = """
                    <html>

                    <head>
                        <title></title>
                    </head>

                    <body>
                        <p style="margin: 0;height: 0;">&nbsp;</p>
                        <p>&nbsp;&nbsp; &nbsp;</p>
                    </body>

                    </html>
                    <title></title>
                    <div style="width:100%;max-width:580px;margin:0 auto;color:#5e616e; font-size: 14px; font-family:PingFang SC">
                        <div style="height:48px;line-height:48px;padding-right: 20px;border-bottom:1px solid #0B6FD6;" width="32"><img height="44" src="" style="border:0;float:left" width="48" /> <span style="font-size:20px;line-height:44px;padding-left:0.5em;font-weight:bold;float:left;">ScanV（云监测）</span></div>
                        <div style="padding: 20px 0;">
                            <div style="font-size: 16px;color: #333333;">尊敬的{user}用户：</div>
                            <p style="margin: 0; text-indent: 20px; line-height: 32px;">您好！</p>
                            <p style="margin: 0; text-indent: 20px; line-height: 32px;">您的资产{name}（{dz}) 监测的可用性{type}任务：{task_name}（{target}），于{time}恢复正常：</p>
                            </div>
                            </br>
                            <div style="width: 100%: height: 42px; line-heihgt: 40px; border: 1px solid #CBD1DA; border-radius: 2px;">
                                异常持续时间：
                                {duration}
                            </div>
                            </br>
                            <div style="text-align: left;">
                            <p style="text-indent:20px">&nbsp;</p>
                            <p>&nbsp;</p>
                            <div style="text-align: center;"><a href="{detail}" style="padding: 10px 20px; background-color: #0B6FD6; color: #fff; text-decoration: none; border-radius: 4px">查看详情</a></div>
                            <p>&nbsp;</p>
                            <p>&nbsp;</p>
                            <div style="font-size: 16px;color: #333">祝好！</div>
                            <div style="font-size: 16px;color: #333">知道创宇云监测</div>
                        </div>
                        <div style="padding: 20px 0; border-top: 1px solid #0B6FD6; height: 80px;"><img src="" style="float: left; margin-right: 12px; width: 80px; height: 80px;" />
                            <div style="float: left; font-size: 14px; line-height: 20px;">
                                <div>此为系统邮件，请勿回复。</div>
                                <div>如您需要任何帮助，欢迎随时联系我们！</div>
                                &nbsp;
                                <div>咨询电话：400-161-0866</div>
                            </div>
                        </div>
                    </div>
                    """
            html = html.format(duration=duration, **kwargs)
        else:

            html_td = """
            <tr style="line-height: 42px;">
                <td style="border: 1px solid #CBD1DA; border-right: 0; padding-left: 20px; border-radius: 4px 0 0 4px;">{area}</td>
                <td style="border: 1px solid #CBD1DA; border-right: 0; padding-left: 20px; border-radius: 4px 0 0 4px;">{ip_type}</td>
                <td style="border: 1px solid #CBD1DA; border-left: 0; padding-right: 20px; border-radius: 0 4px 4px 0; text-align: right;">
                    <img width="16" height="16" style="margin-right:20px;vertical-align: text-bottom;" src="" />
                    {title}
                </td>
            </tr>
                """

            html = """
            <html>

            <head>
                <title></title>
            </head>

            <body>
                <p style="margin: 0;height: 0;">&nbsp;</p>
                <p>&nbsp;&nbsp; &nbsp;</p>
            </body>

            </html>
            <title></title>
            <div style="width:100%;max-width:580px;margin:0 auto;color:#5e616e; font-size: 14px; font-family:PingFang SC">
                <div style="height:48px;line-height:48px;padding-right: 20px;border-bottom:1px solid #0B6FD6;" width="32"><img height="44" src="" style="border:0;float:left" width="48" /> <span style="font-size:20px;line-height:44px;padding-left:0.5em;font-weight:bold;float:left;">ScanV（云监测）</span></div>
                <div style="padding: 20px 0;">
                    <div style="font-size: 16px;color: #333333;">尊敬的{user}用户：</div>
                    <p style="margin: 0; text-indent: 20px; line-height: 32px;">您好！</p>
                    <p style="margin: 0; text-indent: 20px; line-height: 32px;">您的资产{name}（{dz}) 监测的可用性{type}任务：{task_name}（{target}），于{time} 发现有以下{num}个监测点异常：</p>
                    <table border="1" cellpadding="1" cellspacing="1" style="width:500px;">
                        <tbody>
                           {html_td}
                        </tbody>
                    </table>
                    <p style="text-indent:20px">&nbsp;</p>
                    <p>&nbsp;</p>
                    <div style="text-align: center;"><a href="{detail}" style="padding: 10px 20px; background-color: #0B6FD6; color: #fff; text-decoration: none; border-radius: 4px">查看详情</a></div>
                    <p>&nbsp;</p>
                    <p>&nbsp;</p>
                    <div style="font-size: 16px;color: #333">祝好！</div>
                    <div style="font-size: 16px;color: #333">知道创宇云监测</div>
                </div>
                <div style="padding: 20px 0; border-top: 1px solid #0B6FD6; height: 80px;"><img src="" style="float: left; margin-right: 12px; width: 80px; height: 80px;" />
                    <div style="float: left; font-size: 14px; line-height: 20px;">
                        <div>此为系统邮件，请勿回复。</div>
                        <div>如您需要任何帮助，欢迎随时联系我们！</div>
                        &nbsp;
                        <div>咨询电话：400-161-0866</div>
                    </div>
                </div>
            </div>
            """
            html_tds = []
            detail_dict = {d.get("node"): d for d in self.task.result.addition.get("detail", []) if d}
            for warning in self.task.result.warnings:
                title = warning.title
                for node in warning.detail.get("node", []):
                    ip_type = detail_dict.get(node, {}).get("networkDisplayName", "")
                    node_area = detail_dict.get(node, {}).get("area", node)
                    html_tds.append(html_td.format(area=node_area, ip_type=ip_type, title=title))
            html_tds = "\t".join(html_tds)
            html = html.format(html_td=html_tds, **kwargs)
        return html

    def _get_email_param(self):
        self.email_param = {
            'user': self.user.username,
            'name': self.name,
            'time': self.end_time,
            'task_name': self.task.name,
            'target': self.task.target,
            'dz': self.url,
            'type': "HTTP",
            'num': str(self.task.result.addition.get("nodeAbnormalCount", 0)),
            'detail': self.detail_link
        }
        return self.email_param


class PingAlertSender(HttpAlertSender):
    task_type = 'ping'
    task_category = 'aviliable'
    resolved_name = 'Ping可用性恢复'
    task_name = 'Ping可用性监测'
    warning_name = 'Ping可用性异常'

    def _get_email_param(self):
        self.email_param = {
            'user': self.user.username,
            'name': self.name,
            'time': self.end_time,
            'task_name': self.task.name,
            'target': self.task.target,
            'dz': self.url,
            'type': "Ping",
            'num': str(self.task.result.addition.get("nodeAbnormalCount", 0)),
            'detail': self.detail_link
        }

    def _get_wechat_content(self):
        kw3 = f"{self.job.note}（{self.job.targetUrl}）Ping任务" if self.job.note else f"{self.job.targetUrl} Ping任务"
        task_info = f"{self.task.name}（{self.task.target}）" if self.task.name else self.task.target
        if self.task.securityStatus == "warning":
            kw1 = "Ping任务异常告警"
            kw4 = f"{task_info}可用性异常，请及时确认。"
        else:
            kw1 = "Ping任务异常恢复告警"
            kw4 = f"{task_info}可用性恢复正常，请及时确认。"
        return kw1, kw3, kw4


class HostVulAlertSender(VulAlertSender):
    """主机监测"""
    task_type = 'hostVul'
    task_category = 'hostVul'
    task_name = '主机监测'
    warning_name = '主机漏洞'
    email_template = 'scanvmax_host_notification_v2'
    sms_template = '5712894'

    @property
    def detail_link(self):
        return f"{SCANVHOST}/#/task/host/info/{str(self.task.jobId)}"

    def get_warning_level_count_map(self):
        level_count_map = defaultdict(int)
        for warning in self.warnings:
            level = warning.level
            level_count_map[level] += 1
        return level_count_map

    def _get_email_param(self):
        self.email_param = {
            '%user%': [self.user.username],
            '%name%': [self.name],
            '%time%': [self.end_time],
            '%dz%': [self.url],
            '%type%': [self.get_collect_type(self.task.triggerType, self.task.taskSettings.collect)],
            '%count%': [str(len(self.warnings))],
            '%content%': [self._get_wechat_keyword3()],
            '%detail%': [self.detail_link]
        }

    def _get_wechat_content(self):
        kw1 = "主机安全告警"
        kw3 = f"发现漏洞{self.warning_count}个：{self._get_wechat_keyword3()}。"
        kw4 = f"存在{self.warning_count}个主机漏洞威胁，请及时修复。"
        return kw1, kw3, kw4


class ChangeCheckAlertSender(AlertSender):
    """
    篡改监测告警
    """
    task_type = TaskType.change_check.value
    task_category = 'changeCheck'
    task_name = '篡改监测'
    warning_name = '篡改威胁'
    email_template = ''
    sms_template = ''

    @property
    def detail_link(self):
        if self.asset_task:
            if self.is_schedule:
                link = f'{SCANVHOST}/#/job-manage/web-task/history/?asset_task_id={str(self.asset_task.id)}&task_id={str(self.task.id)}&task_types=change_check'
            else:
                link = f'{SCANVHOST}/#/job-manage/web-task/detail?jid={str(self.task.jobId)}&id={str(self.asset_task.id)}&type={self.task.triggerType}&tag=change_check'
        else:
            link = f'{SCANVHOST}/#/job-manage/web-task?target_url={self.job.targetUrl}'
        return link

    def _get_wechat_content(self):
        kw1 = "篡改告警"
        kw3 = f"发现篡改事件{self.warning_count}个：{self._get_wechat_keyword3()}。"
        kw4 = f"存在{self.warning_count}个篡改事件，请及时处理。"
        return kw1, kw3, kw4

    def _get_email_statistics(self):
        warning_type_map = {}
        for warn in self.warnings:
            level = int(warn.level)
            if not warning_type_map.get(level):
                warning_type_map.update({level: {"count": 1, "category": set(warn.detail.keys())}})
            else:
                warning_type_map[level]["count"] += 1
                warning_type_map[level]["category"].update(set(warn.detail.keys()))
        res = []
        for w, d in warning_type_map.items():
            res.append({
                "level_cn": LEVEL_NAME_MAP[w],
                "count": d["count"],
                "level": w,
                "types": "、".join([CHANGE_CHECK_CATEGORY_NAME_MAP[_] for _ in d["category"]])
            })
        return list(sorted(res, key=lambda x:x["level"], reverse=True))

    def _get_html(self):
        html_trs = []
        for w in self._get_email_statistics():
            html_trs.append(f'<tr><td align="center">{w["level_cn"]}</td><td align="center">{w["count"]}</td><td align="center">{w["types"]}</td></tr>')
        html = f"""
        <html>
        <head>
            <title></title>
        </head>
        <body>
            <p style="margin: 0;height: 0;">&nbsp;</p>
            <p>&nbsp;&nbsp; &nbsp;</p>
        </body>
        </html>
        <title></title>
        <div style="width:100%;max-width:580px;margin:0 auto;color:#5e616e; font-size: 14px; font-family:PingFang SC">
            <div style="height:48px;line-height:48px;padding-right: 20px;border-bottom:1px solid #0B6FD6;" width="32"><img height="44" src="" style="border:0;float:left" width="48" /> <span style="font-size:20px;line-height:44px;padding-left:0.5em;font-weight:bold;float:left;">ScanV（云监测）</span></div>
            <div style="padding: 20px 0;">
                <div style="font-size: 16px;color: #333333;">尊敬的{self.user.username}用户：</div>
                <p style="margin: 0; text-indent: 20px; line-height: 32px;">您好！</p>
                <p style="margin: 0; text-indent: 20px; line-height: 32px;">您的资产{self.name}（{self.url}）于{self.end_time}已完成周期性篡改监测。</p>
                <p style="margin: 0; text-indent: 20px; line-height: 32px;">本次监测共发现{self.warning_count}个疑似篡改事件，为了您的资产安全，请及时处理并加强安全防护体系。</p>
                <table border="1" cellpadding="5" cellspacing="0" style="width:565px;">
                    <tr>
                        <th bgcolor="#F5F5F5">威胁等级</th>
                        <th bgcolor="#F5F5F5">风险地址数</th>
                        <th bgcolor="#F5F5F5">涉及变更类型</th>
                    </tr>
                    {"".join(html_trs)}
                </table>
                <p style="text-indent:20px">&nbsp;</p>
                <p>&nbsp;</p>
                <div style="text-align: center;"><a href="{self.detail_link}" style="padding: 10px 20px; background-color: #0B6FD6; color: #fff; text-decoration: none; border-radius: 4px">查看详情</a></div>
                <p>&nbsp;</p>
                <p>&nbsp;</p>
                <div style="font-size: 16px;color: #333">祝好！</div>
                <div style="font-size: 16px;color: #333">知道创宇云监测</div>
            </div>
            <div style="padding: 20px 0; border-top: 1px solid #0B6FD6; height: 80px;"><img src="" style="float: left; margin-right: 12px; width: 80px; height: 80px;" />
                <div style="float: left; font-size: 14px; line-height: 20px;">
                    <div>此为系统邮件，请勿回复。</div>
                    <div>如您需要任何帮助，欢迎随时联系我们！</div>
                    &nbsp;
                    <div>咨询电话：400-161-0866</div>
                </div>
            </div>
        </div>
        """
        return html

    def send_email(self):
        if not self.email_receivers:
            return
        html = self._get_html()
        is_send = False
        subject = f"【ScanV】发现业务系统存在{self.warning_name}"
        for receiver in self.email_receivers:
            try:
                status, resp_text = self.send_cloud.simple_sendmail(to_user=[receiver],
                                                                    subject=subject,
                                                                    html=html)
                app.logger.info(
                    f'邮件发送结果：status: {status}, to receiver: {receiver}, template: {self.email_template}, param: {self.email_param}，resp: {resp_text}')
                if status == "success":
                    is_send = True
            except Exception as e:
                app.logger.info("邮件发送失败：", e, self.email_template, self.email_param)

        # 增加今日已告警次数
        if is_send is True:
            self.inc_email_today_alert()

    def send_sms(self):
        is_send = False
        text = f"【ScanV】{self.name}（{self.url}）于{self.end_time}周期性监测中发现{self.warning_count}个疑似篡改事件，请及时修复。"
        for receiver in self.sms_receivers:
            try:
                status, res_msg = self.yunpian.normal_send(receiver, text)
                app.logger.info(f"短信发送结果：status: {status}, to receiver: {receiver}, template: {self.sms_template}, resp: {res_msg}")
                if status == "success":
                    is_send = True
            except Exception as e:
                app.logger.info("短信发送失败：", e, self.sms_template, self.sms_param)
        # 增加今日已告警次数
        if is_send is True:
            self.inc_sms_today_alert()
